/*
 * Test user_exp.
 *
 * Copyright 2022 and 2023 Odin Kroeger.
 *
 * This file is part of suCGI.
 *
 * suCGI is free software: you can redistribute it and/or modify it
 * under the terms of the GNU Affero General Public License as published
 * by the Free Software Foundation, either version 3 of the License,
 * or (at your option) any later version.
 *
 * suCGI is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General
 * Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public
 * License along with suCGI. If not, see <https://www.gnu.org/licenses/>.
 */

#define _XOPEN_SOURCE 700

#include <assert.h>
#include <err.h>
#include <limits.h>
#include <pwd.h>
#include <setjmp.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>

#include "../macros.h"
#include "../params.h"
#include "../str.h"
#include "../user.h"
#include "types.h"
#include "libutil/abort.h"
#include "libutil/str.h"
#include "libutil/types.h"


/*
 * Data types
 */

/* A mapping of arguments to return values */
typedef struct {
    const char *const fmt;
    const struct passwd *user;
    const size_t bufsize;
    const char *const buf;
    const Error retval;
    const int signal;
} UserExpArgs;


/*
 * Main
 */

int
main(void)
{
    /* RATS: ignore; used safely */
    char logname[] = "jdoe";

    /* RATS: ignore; used safely */
    char homedir[] = "/home/jdoe";

    const struct passwd user = {
        .pw_name = logname,
        .pw_uid = 1000,
        .pw_gid = 1000,
        .pw_dir = homedir
    };

    /* RATS: ignore; used safely */
    char hugestr[MAX_STR_LEN + 1U] _unused = {0};
    (void) memset(hugestr, 'x', sizeof(hugestr) - 1U);

    /* RATS: ignore; used safely */
    char longstr[MAX_STR_LEN] = {0};
    (void) memset(longstr, 'x', sizeof(longstr) - 1U);

    /* RATS: ignore; used safely */
    char longpattern[MAX_STR_LEN] = {0};
    (void) memset(longpattern, 'x', sizeof(longpattern) - sizeof(logname));

    /* RATS: ignore; used safely */
    char longresult[MAX_STR_LEN] = {0};
    (void) memset(longresult, 'x', sizeof(longresult) - sizeof(logname));

    /* RATS: ignore; used safely */
    char hugepattern[MAX_STR_LEN + 1U] = {0};
    (void) memset(hugepattern, 'x', sizeof(hugepattern) - sizeof(logname));

/* Yes, I do mean the use size of the source */
#if defined(__GNUC__) && \
    (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 8))
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wsizeof-pointer-memaccess"
#endif

    /* RATS: ignore; the buffer is large enough */
    (void) strncpy(&longpattern[sizeof(longpattern) - sizeof(logname)],
                   "%n", sizeof("%n"));

    /* RATS: ignore; the buffer is large enough */
    (void) strncpy(&longresult[sizeof(longresult) - sizeof(logname)],
                   logname, sizeof(logname));

    /* RATS: ignore; the buffer is large enough */
    (void) strncpy(&hugepattern[sizeof(hugepattern) - sizeof(logname)],
                   "%n", sizeof("%n"));

#if defined(__GNUC__) && \
    (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 8))
#pragma GCC diagnostic pop
#endif

    const UserExpArgs cases[] = {
        /* Errors */
#if !defined(NDEBUG)
#if defined(NATTR)
        {NULL, &user, MAX_STR_LEN, "", OK, 6},
        {"", NULL, MAX_STR_LEN, "", OK, 6},
        {"", &user, MAX_STR_LEN, NULL, OK, 6},
#endif
        {hugestr, &user, MAX_STR_LEN, "", OK, 6},
        {hugestr, &user, sizeof(hugestr), "", OK, 6},
#endif

        {"%", &user, MAX_STR_LEN, "", ERR_ARGS, 0},
        {"%G", &user, MAX_STR_LEN, "", ERR_ARGS, 0},
        {"", &user, 0, "", ERR_LEN, 0},
        {"x", &user, 0, "", ERR_LEN, 0},
        {hugepattern, &user, MAX_STR_LEN, "", ERR_LEN, 0},

        /* Empty string */
        {"", &user, 1, "", OK, 0},
        {"", &user, MAX_STR_LEN, "", OK, 0},

        /* Long strings */
        {longstr, &user, sizeof(longstr), longstr, OK, 0},
        {longpattern, &user, sizeof(longresult), longresult, OK, 0},

        /* Escaping */
        {"foo", &user, MAX_STR_LEN, "foo", OK, 0},
        {"%%", &user, MAX_STR_LEN, "%", OK, 0},
        {"foo%%bar", &user, MAX_STR_LEN, "foo%bar", OK, 0},
        {"%%foo", &user, MAX_STR_LEN, "%foo", OK, 0},
        {"foo%%", &user, MAX_STR_LEN, "foo%", OK, 0},
        {"foo%%bar%%baz", &user, MAX_STR_LEN, "foo%bar%baz", OK, 0},
        {"%%foo%%bar%%", &user, MAX_STR_LEN, "%foo%bar%", OK, 0},
        {"foo%%bar%%", &user, MAX_STR_LEN, "foo%bar%", OK, 0},

        /* Expand username */
        {"%n", &user, MAX_STR_LEN, "jdoe", OK, 0},
        {"%nfoo", &user, MAX_STR_LEN, "jdoefoo", OK, 0},
        {"foo%nbar", &user, MAX_STR_LEN, "foojdoebar", OK, 0},
        {"foo%n", &user, MAX_STR_LEN, "foojdoe", OK, 0},

        /* Expand home directory */
        {"%d", &user, MAX_STR_LEN, "/home/jdoe", OK, 0},
        {"%d/foo", &user, MAX_STR_LEN, "/home/jdoe/foo", OK, 0},
        {"foo%d/bar", &user, MAX_STR_LEN, "foo/home/jdoe/bar", OK, 0},
        {"foo%d", &user, MAX_STR_LEN, "foo/home/jdoe", OK, 0},

        /* Expand UID */
        {"%u", &user, MAX_STR_LEN, "1000", OK, 0},
        {"%ufoo", &user, MAX_STR_LEN, "1000foo", OK, 0},
        {"foo%ubar", &user, MAX_STR_LEN, "foo1000bar", OK, 0},
        {"foo%u", &user, MAX_STR_LEN, "foo1000", OK, 0},

        /* Expand GID */
        {"%g", &user, MAX_STR_LEN, "1000", OK, 0},
        {"%gfoo", &user, MAX_STR_LEN, "1000foo", OK, 0},
        {"foo%gbar", &user, MAX_STR_LEN, "foo1000bar", OK, 0},
        {"foo%g", &user, MAX_STR_LEN, "foo1000", OK, 0},

        /* Combinations */
        {"%d/%g/%n", &user, MAX_STR_LEN, "/home/jdoe/1000/jdoe", OK, 0},
        {"%d/%g/%nxxx", &user, MAX_STR_LEN, "/home/jdoe/1000/jdoexxx", OK, 0},
        {"xxx%d/%g/%u", &user, MAX_STR_LEN, "xxx/home/jdoe/1000/1000", OK, 0},

        /* Unicode */
        {"𝑓𝑜𝑜", &user, MAX_STR_LEN, "𝑓𝑜𝑜", OK, 0},
        {"%%", &user, MAX_STR_LEN, "%", OK, 0},
        {"𝑓𝑜𝑜%%𝔟𝔞𝔯", &user, MAX_STR_LEN, "𝑓𝑜𝑜%𝔟𝔞𝔯", OK, 0},
        {"%%𝑓𝑜𝑜", &user, MAX_STR_LEN, "%𝑓𝑜𝑜", OK, 0},
        {"𝑓𝑜𝑜%%", &user, MAX_STR_LEN, "𝑓𝑜𝑜%", OK, 0},
        {"𝑓𝑜𝑜%%𝔟𝔞𝔯%%𝚋𝚊𝚣", &user, MAX_STR_LEN, "𝑓𝑜𝑜%𝔟𝔞𝔯%𝚋𝚊𝚣", OK, 0},
        {"%%𝑓𝑜𝑜%%𝔟𝔞𝔯%%", &user, MAX_STR_LEN, "%𝑓𝑜𝑜%𝔟𝔞𝔯%", OK, 0},
        {"𝑓𝑜𝑜%%𝔟𝔞𝔯%%", &user, MAX_STR_LEN, "𝑓𝑜𝑜%𝔟𝔞𝔯%", OK, 0},
        {"%n", &user, MAX_STR_LEN, "jdoe", OK, 0},
        {"%n𝑓𝑜𝑜", &user, MAX_STR_LEN, "jdoe𝑓𝑜𝑜", OK, 0},
        {"𝑓𝑜𝑜%n𝔟𝔞𝔯", &user, MAX_STR_LEN, "𝑓𝑜𝑜jdoe𝔟𝔞𝔯", OK, 0},
        {"𝑓𝑜𝑜%n", &user, MAX_STR_LEN, "𝑓𝑜𝑜jdoe", OK, 0},
        {"%d", &user, MAX_STR_LEN, "/home/jdoe", OK, 0},
        {"%d/𝑓𝑜𝑜", &user, MAX_STR_LEN, "/home/jdoe/𝑓𝑜𝑜", OK, 0},
        {"𝑓𝑜𝑜%d/𝔟𝔞𝔯", &user, MAX_STR_LEN, "𝑓𝑜𝑜/home/jdoe/𝔟𝔞𝔯", OK, 0},
        {"𝑓𝑜𝑜%d", &user, MAX_STR_LEN, "𝑓𝑜𝑜/home/jdoe", OK, 0},
        {"%u", &user, MAX_STR_LEN, "1000", OK, 0},
        {"%u𝑓𝑜𝑜", &user, MAX_STR_LEN, "1000𝑓𝑜𝑜", OK, 0},
        {"𝑓𝑜𝑜%u𝔟𝔞𝔯", &user, MAX_STR_LEN, "𝑓𝑜𝑜1000𝔟𝔞𝔯", OK, 0},
        {"𝑓𝑜𝑜%u", &user, MAX_STR_LEN, "𝑓𝑜𝑜1000", OK, 0},
        {"%g", &user, MAX_STR_LEN, "1000", OK, 0},
        {"%g𝑓𝑜𝑜", &user, MAX_STR_LEN, "1000𝑓𝑜𝑜", OK, 0},
        {"𝑓𝑜𝑜%g𝔟𝔞𝔯", &user, MAX_STR_LEN, "𝑓𝑜𝑜1000𝔟𝔞𝔯", OK, 0},
        {"𝑓𝑜𝑜%g", &user, MAX_STR_LEN, "𝑓𝑜𝑜1000", OK, 0},
        {"%d/%g/%n", &user, MAX_STR_LEN, "/home/jdoe/1000/jdoe", OK, 0},
        {"%d/%g/%n𝓍𝓍𝓍", &user, MAX_STR_LEN, "/home/jdoe/1000/jdoe𝓍𝓍𝓍", OK, 0},
        {"𝓍𝓍𝓍%d/%g/%u", &user, MAX_STR_LEN, "𝓍𝓍𝓍/home/jdoe/1000/1000", OK, 0}
    };

    volatile int result = PASS;

    for (volatile size_t i = 0; i < NELEMS(cases); ++i) {
        const UserExpArgs args = cases[i];
        /* RATS: ignore; used safely */
        char buf[MAX_STR_LEN + 1U];
        size_t calclen = 0;

        assert(args.bufsize <= sizeof(buf));

        if (sigsetjmp(abort_env, 1) == 0) {
            Error retval;

            if (args.signal != 0) {
                warnx("the next test should fail an assertion.");
            }

            (void) abort_catch(err);
            if (args.buf == NULL) {
#if (!defined(NDEBUG) || !NDEBUG) && (defined(NATTR) && NATTR)
                retval = user_exp(
                    args.fmt,
                    args.user,
                    args.bufsize,
                    /* cppcheck-suppress nullPointer; bad input is intended */
                    NULL,
                    &calclen
                );
#else
                retval = OK;
#endif
            } else {
                retval = user_exp(args.fmt, args.user,
                                  args.bufsize, buf, &calclen);
            }
            (void) abort_reset(err);

            if (retval != args.retval) {
                result = FAIL;
                warnx("(%s, <user>, %zu, -> %s, -> %zu) -> %u != %u",
                      args.fmt, args.bufsize, buf,
                      calclen, retval, args.retval);
            }

            if (retval == args.retval && retval == OK) {
                size_t reallen = strnlen(buf, MAX_STR_LEN + 1U);
                assert(reallen < MAX_STR_LEN + 1U);

                if (calclen != reallen) {
                    result = FAIL;
                    warnx("(%s, <user>, %zu, -> %s, -> %zu != %zu) -> %u",
                          args.fmt, args.bufsize, buf,
                          calclen, reallen, retval);
                }

                if (args.buf && strncmp(buf, args.buf, reallen + 1U) != 0) {
                    result = FAIL;
                    warnx("(%s, <user>, %zu, -> %s != %s, -> %zu) -> %u",
                          args.fmt, args.bufsize, buf, args.buf,
                          calclen, retval);
                }
            }
        }

        if (abort_signal != args.signal) {
            result = FAIL;
            warnx("(%s, <user>, %zu, -> %s, -> %zu) ^ %d != %d",
                  args.fmt, args.bufsize, buf, calclen,
                  (int) abort_signal, (int) args.signal);
        }
    }

    return result;
}
